/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using EnvDTE;
using System.ComponentModel;
using VSLangProj80;
using DaveSexton.DocProject.Engine;
using System.Collections.ObjectModel;
using DaveSexton.DocProject.Configuration;
using System.Drawing.Design;
using System.ComponentModel.Design;
using System.Windows.Forms.Design;

namespace DaveSexton.DocProject
{
	[TypeConverter(typeof(SortedPropertyConverter))]
	public abstract class DocProjectOptions : Component
	{
		#region Public Properties
		/// <summary>
		/// Gets or sets whether the user will be prompted to include certain, pre-determined files and folders in the project after the build
		/// has completed.  The default value is false.
		/// </summary>
		/// <remarks>
		/// In the dialog displayed to the user a checkbox is presented to control the value of this property.  Also, the user can set the 
		/// value of this property in the properties window of the project.
		/// </remarks>
		/// <seealso cref="DefaultIncludeOutputInProject" />
		/// <seealso cref="DefaultApplyToAll" />
		[Category("Include Project Output Dialog"), DisplayName("Don't ask me this again"), DefaultValue(false), 
		Description("Specifies whether the user will be prompted to include certain, predetermined files and folders in the project after the build has completed.")]
		public bool DoNotAskIncludeOutputInProject
		{
			get
			{
				return project.Settings.Read<bool>("DoNotAskIncludeOutputInProject");
			}
			set
			{
				project.Settings.Write("DoNotAskIncludeOutputInProject", value);
			}
		}

		/// <summary>
		/// Gets or sets whether certain, pre-determined files and folders will be included in the project after the build has completed.  The value
		/// is only used when <see cref="DoNotAskIncludeOutputInProject" /> is true.  The default value is false.
		/// </summary>
		/// <remarks>
		/// This property is only used when the value of <see cref="DoNotAskIncludeOutputInProject" /> is true.
		/// </remarks>
		/// <seealso cref="DoNotAskIncludeOutputInProject" />
		/// <seealso cref="DefaultApplyToAll" />
		[Category("Include Project Output Dialog"), DisplayName("Include project output default"), DefaultValue(false), 
		Description("When \"Don't ask me this again\" is true, specifies whether certain, predetermined files and folders will be included in the project after the build has completed.")]
		public bool DefaultIncludeOutputInProject
		{
			get
			{
				return project.Settings.Read<bool>("DefaultIncludeOutputInProject");
			}
			set
			{
				project.Settings.Write("DefaultIncludeOutputInProject", value);
			}
		}

		/// <summary>
		/// Gets or sets whether &quot;Apply to all&quot; will be checked when the user is prompted to include certain, pre-determined files and 
		/// folders in the project after the build has completed.  The default value is false.
		/// </summary>
		/// <remarks>
		/// In the dialog displayed to the user a checkbox is presented for which this property controls its value the first time the dialog
		/// is displayed after a build has completed.  The user can set the value of this property through that checkbox or in the properties 
		/// window of the project.
		/// </remarks>
		/// <seealso cref="DefaultIncludeOutputInProject" />
		/// <seealso cref="DoNotAskIncludeOutputInProject" />
		[Category("Include Project Output Dialog"), DisplayName("Apply to all default"), DefaultValue(false), 
		Description("Specifies whether \"Apply to all\" will be checked when the user is prompted to include certain, predetermined files and folders in the project after the build has completed.")]
		public bool DefaultApplyToAll
		{
			get
			{
				return project.Settings.Read<bool>("DefaultApplyToAll");
			}
			set
			{
				project.Settings.Write("DefaultApplyToAll", value);
			}
		}

		[Category("Build"), DisplayName("DocProject version"), Description("The version of DocProject that was used to create this project."),
		SortedProperty(-2), TypeConverter(typeof(DocProjectVersionTypeConverter))]
		public string DocProjectVersion
		{
			get
			{
				return GetDocProjectVersion(project);
			}
		}

		/// <summary>
		/// Gets the provider name of the build engine being used by the DocProject.
		/// </summary>
		[Category("Build"), DisplayName("Build engine"), TypeConverter(typeof(DocProjectOptions.BuildEngineProviderNameTypeConverter)),
		SortedProperty(-1), Description("Indicates the build engine being used by the DocProject or DocSite.")]
		public virtual string BuildEngineProviderName
		{
			get
			{
				return GetBuildEngineProviderName(project, DocProjectEnvironment.DefaultBuildEngineProvider.Name);
			}
		}

		/// <summary>
		/// Gets or sets the scope for documentation to be included in the build output.  This setting can be used to configure the inclusion of 
		/// internal members or to restrict the documentation to public members only.  The default value is <see cref="DocumentationScope" />.PublicOnly.
		/// </summary>
		[Category("Build"), DisplayName("Documentation scope"), DefaultValue(DocumentationScope.PublicOnly),
		Description("Specifies the scope for documentation to be included in the build output.  This setting can be used to configure the inclusion of internal members or to restrict the documentation to public members only.")]
		public DocumentationScope DocumentationScope
		{
			get
			{
				return project.Settings.Read("DocumentationScope", DocumentationScope.PublicOnly);
			}
			set
			{
				if (!Enum.IsDefined(typeof(DocumentationScope), value))
					throw new System.ComponentModel.InvalidEnumArgumentException("value", (int) value, typeof(DocumentationScope));

				if (DocumentationScope != value)
				{
					project.Settings.Write("DocumentationScope", value);

					NotifyFullBuildRequired();
				}
			}
		}

		[Category("Build"), DisplayName("External sources"), Description("List of external files that will be included in the generated help.")]
		[Editor(typeof(ExternalSourcesEditor), typeof(UITypeEditor)), TypeConverter(typeof(ExternalSourceTypeConverter))]
		public SourceCollection ExternalSources
		{
			get
			{
				return project.ExternalSources;
			}
			internal set
			{
				project.Settings.Write("ExternalSources", value.ToListString(project.Directory));

				if (BufferChanges)
					bufferedExternalSources = value;
				else
					CommitExternalSources(value);

				NotifyFullBuildRequired();
			}
		}

		/// <summary>
		/// Gets or sets the full name of a type that derives from <see cref="BuildProcessComponent" /> in the assembly output to receive 
		/// notifications of various build events.
		/// </summary>
		/// <value>The full name of a class (namespace and type name) with a public, parameterless constructor that derives from <see cref="BuildProcessComponent" />.</value>
		[Category("Extensibility"), DisplayName("Process component type name"),
		Description("Specifies a type that derives from BuildProcessComponent to receive notifications of various build events.  " + 
		 @"Enter a full type name, assembly-qualified type name or a full type name with an absolute or relative assembly path, separated by a vertical bar; " + 
		 @"e.g., MyNamespace.MyType|..\anotherproj\bin\Debug\anotherproj.dll")]
		public string ProcessComponentTypeName
		{
			get
			{
				return project.Settings.Read<string>("ProcessComponentTypeName");
			}
			set
			{
				if (!string.Equals(ProcessComponentTypeName, value))
				{
					project.Settings.Write("ProcessComponentTypeName", value);

					if (!string.IsNullOrEmpty(value))
						NotifyFullBuildRequired();
				}
			}
		}

		[Browsable(false)]
		public IDocProject Project
		{
			get
			{
				return project;
			}
		}

		[Browsable(false)]
		public bool BufferChanges
		{
			get
			{
				return project.Settings.UseBuffer;
			}
			set
			{
				project.Settings.UseBuffer = value;
			}
		}
		#endregion

		#region Private / Protected
		private readonly IDocProject project;
		private SourceCollection bufferedExternalSources;
		private volatile bool fullBuildRequired;
		#endregion

		#region Constructors
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
		static DocProjectOptions()
		{
			DocProjectEnvironment.ProjectSaved += DocProjectEnvironment_ProjectSaved;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="DocProjectOptions" /> class for derived classes.
		/// </summary>
		/// <remarks>
		/// <strong>Note to inheritors</strong>
		/// <para>
		/// In constructors for derived classes, do not attempt to access the <see cref="BuildSettings"/> for the related <see cref="BuildEngine"/>.  
		/// Doing so may cause DocProject to use the <see cref="BuildEngineProvider"/> to create an instance of the <strong>BuildSettings</strong> 
		/// class and if the constructor for that class attempts to use the related <see cref="DocProjectOptions"/> then the provider may be asked 
		/// to create the options as well, resulting in an infinite loop.
		/// </para>
		/// </remarks>
		protected DocProjectOptions(IDocProject project)
		{
			if (project == null)
				throw new ArgumentNullException("project");

			this.project = project;
		}
		#endregion

		#region Methods
		public static void ImportOptions(IDocProject fromProject, IDocProject toProject, bool importExternalSources)
		{
			if (fromProject == null)
				throw new ArgumentNullException("fromProject");

			if (toProject == null)
				throw new ArgumentNullException("toProject");

			if (importExternalSources)
				AddExternalSources(toProject, fromProject.ExternalSources);

			ImportSetting<DocumentationScope>("DocumentationScope", fromProject, toProject);
			ImportSetting<bool>("DefaultApplyToAll", fromProject, toProject);
			ImportSetting<bool>("DefaultIncludeOutputInProject", fromProject, toProject);
			ImportSetting<bool>("DoNotAskIncludeOutputInProject", fromProject, toProject);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
		protected static void ImportSetting(string name, IDocProject fromProject, IDocProject toProject)
		{
			string value = fromProject.Settings.Read(name, (string) null);

			if (value != null)
				toProject.Settings.Write(name, value);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
		protected static void ImportSetting<T>(string name, IDocProject fromProject, IDocProject toProject)
			where T : struct
		{
			T? value = fromProject.Settings.Read(name, (T?) null);

			if (value != null)
				toProject.Settings.Write(name, value.Value);
		}

		public static void AddExternalSources(IDocProject project, SourceCollection sources)
		{
			if (project == null)
				throw new ArgumentNullException("project");

			if (sources == null)
				throw new ArgumentNullException("sources");

			SourceCollection existing = new SourceCollection(project.Provider.AllSupportedExternalSourceFileExtensions);
			existing.AddFromList(project.Directory, project.Settings.Read("ExternalSources"));
			existing.AddRange(sources);

			project.Settings.Write("ExternalSources", existing.ToListString(project.Directory));

			SourceCollection savedSources = project.ExternalSources;

			savedSources.Clear();

			foreach (Source source in existing)
				savedSources.Add(source);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
		public static string GetBuildEngineProviderName(IDocProject project, string defaultName)
		{
			if (project == null)
				throw new ArgumentNullException("project");

			return project.Settings.Read("BuildEngineProviderName", defaultName);
		}

		[System.Diagnostics.DebuggerHidden]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
		public static string GetDocProjectVersion(IDocProject project)
		{
			try
			{
				string value = project.Settings.GetCommittedValue("DocProjectVersion");

				if (!string.IsNullOrEmpty(value))
					return new Version(value).ToString();
			}
			catch (ArgumentException) { }
			catch (OverflowException) { }
			catch (FormatException) { }

			return null;
		}

		internal static void SetDocProjectVersion(IDocProject project, Version version)
		{
			project.Settings.SetCommittedValue("DocProjectVersion", version.ToString());
		}

		public void NotifyFullBuildRequired()
		{
			fullBuildRequired = true;
		}

		internal static void SetBuildEngineProvider(IDocProject project, string name)
		{
			project.Settings.Write("BuildEngineProviderName", name);

			// setting the provider to null will cause the Provider property to reload its backing field
			// the next time it's read.  (must occur before the changed event is raised)
			project.Provider = null;
		}

		public object ShowPropertyDialog(string propertyName, IServiceProvider provider)
		{
			if (string.IsNullOrEmpty(propertyName))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "propertyName");

			PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(this);

			PropertyDescriptor property = properties[propertyName];

			if (property == null)
				throw new ArgumentException(string.Format(System.Globalization.CultureInfo.CurrentCulture, 
					Resources.Errors.InvalidPropertyName, propertyName), "propertyName");

			ITypeDescriptorContext context = new TypeDescriptorContext(this, provider, property);

			UITypeEditor editor = (UITypeEditor) property.GetEditor(typeof(UITypeEditor));

			if (editor == null || editor.GetEditStyle(context) != UITypeEditorEditStyle.Modal)
				throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Resources.Errors.InvalidPropertyEditor, propertyName));

			object value = editor.EditValue(context, provider, property.GetValue(this));

			property.SetValue(this, value);

			return value;
		}

		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		/// <remarks>
		/// This method is only called by the Tools Options page when OK is clicked.  Project settings are automatically buffered
		/// while the dialog is opened; however, custom project options must be bufferred manually.
		/// </remarks>
		public virtual void Commit()
		{
			if (bufferedExternalSources != null)
			{
				CommitExternalSources(bufferedExternalSources);
				bufferedExternalSources = null;
			}
		}

		private void CommitExternalSources(SourceCollection value)
		{
			SourceCollection sources = project.ExternalSources;

			sources.Clear();

			foreach (Source source in value)
				sources.Add(source);
		}

		/// <remarks>
		/// This method is only called by the Tools Options page when it's canceled.  Project settings are automatically buffered
		/// while the dialog is opened; however, custom project options must be bufferred manually.
		/// </remarks>
		public virtual void Cancel()
		{
			bufferedExternalSources = null;
		}

		/// <remarks>
		/// This method is called when the project is saved so that derived types can save custom options to disc.  Custom options are those that 
		/// do not use the project's <see cref="IAnyProject.Settings">Settings</see> as a backing store.
		/// </remarks>
		public virtual void Save()
		{
		}

		/// <summary>
		/// Notifies the component that a property has changed.
		/// </summary>
		/// <remarks>
		/// DocProject's external UI program assigns the <see cref="IComponent.Site"/> property of this class.
		/// The <see cref="ISite"/> implementation provides an implementation of the <see cref="IComponentChangeService"/>
		/// that can be used to notify the program when a property has changed.  The service is used by the 
		/// <see cref="PropertyGrid"/> control to determine whether the <see cref="PropertyValueChanged"/> event should
		/// be raised for read-only properties that use a custom editor.  For example, collection editors change the 
		/// collection's contents but do not change the reference itself.  Notification is not required for normal
		/// read-write properties, such as <see cref="DocumentationScope"/>.
		/// </remarks>
		public virtual void NotifyChanged<T>(string propertyName, T oldValue, T newValue)
		{
			if (!BufferChanges)
				project.IsSaved = false;

			ISite site = Site;

			if (site != null)
			{
				IComponentChangeService service = site.GetService(typeof(IComponentChangeService)) as IComponentChangeService;

				if (service != null)
				{
					PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(this);

					PropertyDescriptor property = properties[propertyName];

					if (property == null)
						throw new ArgumentException(string.Format(System.Globalization.CultureInfo.CurrentCulture, 
							Resources.Errors.NotifyChangedPropertyNameInvalid, propertyName), "propertyName");

					service.OnComponentChanged(this, property, oldValue, newValue);
				}
			}
		}

		public virtual void NotifyChanging(string propertyName)
		{
			ISite site = Site;

			if (site != null)
			{
				IComponentChangeService service = site.GetService(typeof(IComponentChangeService)) as IComponentChangeService;

				if (service != null)
				{
					PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(this);

					PropertyDescriptor property = properties[propertyName];

					if (property == null)
						throw new ArgumentException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
							Resources.Errors.NotifyChangedPropertyNameInvalid, propertyName), "propertyName");

					service.OnComponentChanging(this, property);
				}
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		public virtual T GetService<T>()
			where T : class
		{
			return GetService<T>(null);
		}

		public new object GetService(Type serviceType)
		{
			return GetService(serviceType, null);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		public virtual T GetService<T>(IServiceProvider provider)
			where T : class
		{
			T service = null;

			if (provider != null)
				service = (T) provider.GetService(typeof(T));

			if (service == null)
			{
				ISite site = Site;

				if (site != null)
					service = (T) site.GetService(typeof(T));
			}

			return service;
		}

		public virtual object GetService(Type serviceType, IServiceProvider provider)
		{
			object service = null;

			if (provider != null)
				service = provider.GetService(serviceType);

			if (service == null)
			{
				ISite site = Site;

				if (site != null)
					service = site.GetService(serviceType);
			}

			return service;
		}

		private bool ShouldSerializeExternalSources()
		{
			return false;
		}

		private bool ShouldSerializeProcessComponentTypeName()
		{
			return !string.Equals(ProcessComponentTypeName, project.Settings.GetCommittedValue("ProcessComponentTypeName"),
				(project.IsVisualBasic) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
		}

		private void ResetProcessComponentTypeName()
		{
			ProcessComponentTypeName = project.Settings.GetCommittedValue("ProcessComponentTypeName");
		}
		#endregion

		#region Event Handlers
		private static void DocProjectEnvironment_ProjectSaved(object sender, DocProjectEventArgs e)
		{
			IDocProject project = e.Project;

			if (project != null)
			{
				DocProjectOptions options = project.Options;

				if (options.fullBuildRequired)
				{
					options.fullBuildRequired = false;

					BuildController.GetEngineForProject(project).BuildType = BuildType.Full;
				}

				options.Save();
			}
		}
		#endregion

		#region Nested
		private sealed class TypeDescriptorContext : ITypeDescriptorContext
		{
			#region Public Properties
			#endregion

			#region Private / Protected
			private readonly DocProjectOptions options;
			private readonly IServiceProvider provider;
			private readonly PropertyDescriptor property;
			private readonly IComponentChangeService change;
			private object oldValue;
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="TypeDescriptorContext" /> class.
			/// </summary>
			public TypeDescriptorContext(DocProjectOptions options, IServiceProvider provider, PropertyDescriptor property)
			{
				this.options = options;
				this.provider = provider;
				this.property = property;

				this.oldValue = property.GetValue(options);

				change = (IComponentChangeService) GetService(typeof(IComponentChangeService));
			}
			#endregion

			#region Methods

			#endregion

			#region ITypeDescriptorContext Members
			public IContainer Container
			{
				get
				{
					return options.Container;
				}
			}

			public object Instance
			{
				get
				{
					return options;
				}
			}

			public void OnComponentChanged()
			{
				object newValue = property.GetValue(options);

				if (change != null)
					change.OnComponentChanged(options, property, oldValue, newValue);

				oldValue = newValue;
			}

			public bool OnComponentChanging()
			{
				if (change != null)
					change.OnComponentChanging(options, property);

				return true;
			}

			public PropertyDescriptor PropertyDescriptor
			{
				get
				{
					return property;
				}
			}

			#endregion

			#region IServiceProvider Members
			public object GetService(Type serviceType)
			{
				object value = options.GetService(serviceType);

				if (value == null && provider != null)
					value = provider.GetService(serviceType);

				return value;
			}
			#endregion
		}

		private sealed class DocProjectVersionTypeConverter : TypeConverter
		{
			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="DocProjectVersionTypeConverter" /> class.
			/// </summary>
			public DocProjectVersionTypeConverter()
			{
			}
			#endregion

			#region Methods
			public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
			{
				DocProjectOptions options = (context == null) ? null : context.Instance as DocProjectOptions;

				if (options != null && destinationType == typeof(string))
				{
					StringBuilder result = new StringBuilder(Resources.Text.DocProject);

					string version = GetDocProjectVersion(options.Project);

					if (!string.IsNullOrEmpty(version))
						result.AppendFormat(" {0}", version);
					else
						result.Append(" (unknown version)");

					return result.ToString();
				}

				return base.ConvertTo(context, culture, value, destinationType);
			}
			#endregion
		}

		private sealed class BuildEngineProviderNameTypeConverter : TypeConverter
		{
			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="BuildEngineProviderNameTypeConverter" /> class.
			/// </summary>
			public BuildEngineProviderNameTypeConverter()
			{
			}
			#endregion

			#region Methods
			public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
			{
				return true;
			}

			public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
			{
				return true;
			}

			public override TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
			{
				Collection<string> values = new Collection<string>();

				foreach (BuildEngineProvider provider in DocProjectEnvironment.BuildEngineProviders)
					values.Add(provider.Name);

				return new StandardValuesCollection(values);
			}

			public override bool IsValid(ITypeDescriptorContext context, object value)
			{
				string str = value as string;

				if (string.IsNullOrEmpty(str))
					return false;

				foreach (BuildEngineProvider provider in DocProjectEnvironment.BuildEngineProviders)
					if (provider.Name.Equals(str, StringComparison.OrdinalIgnoreCase))
						return true;

				return false;
			}
			#endregion
		}
		#endregion
	}
}
